package com.atguigu.tingshu.common.login.aspect;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.tingshu.common.constant.PublicConstant;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Author:huzhongkui
 * Date: 2025-01-13 星期一 15:12:16
 * Description:  认证中心
 * 认证中心：
 * 1、Aop (选择的)
 * 2、过滤器 网关
 * 3、拦截器  SpringMvc
 */

@Component
@Aspect
public class LoginAspect {


    @Autowired
    private StringRedisTemplate redisTemplate;


    /**
     * 切面类：
     * 1、切面的通知（切面的逻辑是什么）
     * 切面通知的类型有5种
     * 1.1 前置通知  获取不到目标方法的返回值
     * 用法：只需要在切面类中的方法中指定一个@Before注解即可
     * 特点：目标方法执行之前，先来执行前置通知。
     * 1.2 返回通知 可以获取到目标方法的返回值
     * 用法： 只需要在切面类中的方法中指定一个@AfterReturning注解即可
     * 特点：目标方法执行过程中没有出现异常，然后在目标方法执行完，在来执行返回通知
     * 1.3 后置通知 获取不到目标方法的返回值
     * 用法： 只需要在切面类中的方法中指定一个@After注解即可
     * 特点： 目标方法执行过程中不管是否出现异常，然后在目标方法执行完，都在来执行后置通知
     * 1.4 异常通知 获取不到目标方法的返回值 但是可以获取到目标方法执行期间的异常对象
     * 用法： 只需要在切面类中的方法中指定一个AfterThrowing注解即可
     * 特点： 标方法执行过程中出现异常，会执行到异常通知。
     * 1.5 环绕通知（上面四种通知的结合体） 获取不到目标方法的返回值对象 但是可以获取到pjp的目标对象（进一步获取到目标方法的返回值对象） 选择的是这一种
     * 用法： 只需要在切面类中的方法中指定一个Around注解即可
     * 特点: 执行目标方法之前，会先执行环绕通知的前置逻辑，然后去执行目标方法（是否出现了异常） 都会继续执行环绕通知的后置逻辑。
     * <p>
     * 2、切入点表达式（这段逻辑对谁起作用）  注解形式
     * 如果在多切面情况下，可能会出现事务失效
     * 假设：登录切面  事务切面 都存在，登录切面把异常catch住之后直接记录日志，没有抛出去 那么事务注解感知不到异常，就会失效
     * 假设：登录切面  缓存切面 都存在 不存在事务 不会有事务失效
     * <p>
     * ThreadLocal可能会出现内存泄漏问题?oom
     * 内存泄漏可能会导致oom
     * oom:概念：内存不够用了。
     * 概念：往内存中存放的数据，用完之后没有移除掉。那么就导致这个数据还在继续占用内存的空间。（只要往threadlocal中存放的数据都是强引用类型）
     * 内存泄漏的解决办法：自己手动移除
     * jvm:
     * 对象有四种类型：强 软 弱 虚
     * 强引用类型的对象：new  出来的对象或者通过反射获取到的对象
     * 强引用类型的对象特点：不会被gc垃圾回收器回收掉，所以当内存中都是强引用类型的对象时，可能会出现oom.(就算报oom 也不会移除强引用类型的对象)
     * 软引用类型的对象特点： oom之前，只要发现内存中有软引用类型的对象，就会来回收
     * 弱引用类型的对象特点： 在gc之前 只要发现内存中有弱引用类型的对象，就会来回收
     * <p>
     * <p>
     * 如果是自己new的线程，这个线程在执行任务期间需要用到ThreadLocal,做法 创建一个ThreadLocal对象 然后将数据丢进去 接着任务使用 当任务做完，可以不亲手动移除value值。
     * 如果是线程池的线程执行任务，这个线程在执行任务期间需要用到ThreadLocal,做法 创建一个ThreadLocal对象 然后将数据丢进去 接着任务使用 当任务做完，一定要手动移除value值。不然就会出现内存泄漏，。从而可能会导致oom
     * 原因：
     * new出来的线程当任务做完之后，这个线程自己就会被gc回收掉从而这个线程对象内部的Map以及Map中的数据都跟着会移除掉。
     * 线程池中的线程当任务做完之后，这个线程不会销毁（不会被gc回收掉） 从而这个线程对象内部的Map以及Map中的数据都不会移除掉。因此要手动移除
     * <p>
     * <p>
     * ThreadLocal存放在Thread(线程)中
     * Thread中存放的是ThreadLocal类中的一个静态内部类叫做ThreadLocalMap
     * <p>
     * ThreadLocalMap本质就是一个Map 该Map的key:就是ThreadLocal对象 该Map的value就是你放的数据。
     */

    @Around(value = "@annotation(com.atguigu.tingshu.common.login.annotation.TingshuLogin)")
    public Object checkLogin(ProceedingJoinPoint pjp) throws Throwable {


        // 1.获取前端请求中请求头的token值
        // 2.校验redis中的token值和前端提交携带过来的是否相等
        // 3.jwt验签和解密数据 得到载荷 进一步得到用户id
        // 4.根据用户id 去到user_info表查询用户信息
        // 5.返回用户信息
        // start stopwatch
        // 前置通知
        // 1.获取前端请求中请求头的token值
        String accessToken = getAccessTokenFromFront();
        // 2.校验accessToken && 获取用户id
        Long userId = checkAccessTokenAndGetUserId(accessToken);

        AuthContextHolder.setUserId(userId);
        Object retVal;  // 执行目标方法
        try {
            retVal = pjp.proceed();
        } finally {
            AuthContextHolder.removeUserId();  // 把存放到ThreadLocal中的value移除掉。
        }
        // 后置通知
        return retVal;
    }


    private Long checkAccessTokenAndGetUserId(String accessToken) {

        // 1. 校验前端是否携带accessToken
        if (StringUtils.isEmpty(accessToken)) {
            throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
        }

        // 2.校验accessToken是否携带对了（1、accessToken是否被篡改过 2、accessToken是否和服务端存的是同一个）
        // 2.1 通过jwt验签来判断（只要验签通过，一定没有被篡改）
        Jwt jwt = JwtHelper.decodeAndVerify(accessToken, new RsaVerifier(PublicConstant.PUBLIC_KEY));

        String claims = jwt.getClaims();
        Map map = JSONObject.parseObject(claims, Map.class);
        String openId = (String) map.get("openId");
        Object userId = map.get("userId");

        // 2.2 accessToken是否和服务端(redis)存的是同一个
        String accessTokenKey = RedisConstant.USER_LOGIN_KEY_PREFIX + openId;
        String fromRedisJwt = redisTemplate.opsForValue().get(accessTokenKey);
        if (StringUtils.isEmpty(fromRedisJwt) || !accessToken.equals(fromRedisJwt)) {
            throw new GuiguException(401, "accessToken过期了");
        }
        // 2.3 直接返回用户id
        return Long.parseLong(userId + "");
    }

    private String getAccessTokenFromFront() {

        // 1.获取请求对象
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        // 2.获取请求对象
        HttpServletRequest request = requestAttributes.getRequest();

        // 3.获取请求头的token值
        String refreshToken = request.getHeader("token");

        return refreshToken;

    }


}
